package org.polyforms.repository.datanucleus;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import javax.persistence.EntityManager;

import org.datanucleus.metadata.AbstractMemberMetaData;
import org.datanucleus.metadata.FieldMetaData;
import org.datanucleus.metadata.PropertyMetaData;
import org.polyforms.repository.spi.IdentifierHelper;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

/**
 * Implementation of {@link IdentifierHelper} for Datanucleus.
 * 
 * @author Kuisong Tong
 * @since 1.0
 */
public final class DatanucleusIdentifierHelper implements IdentifierHelper {
    @SuppressWarnings("unchecked")
    private final Map<Class<? extends AbstractMemberMetaData>, ValueGetter> valueGetters = new HashMap<Class<? extends AbstractMemberMetaData>, ValueGetter>();

    private final EntityManager entityManager;

    /**
     * Create an instance with {@link EntityManager}.
     */
    public DatanucleusIdentifierHelper(final EntityManager entityManager) {
        this.entityManager = entityManager;
        valueGetters.put(FieldMetaData.class, new FieldValueGetter());
        valueGetters.put(PropertyMetaData.class, new PropertyValueGetter());
    }

    /**
     * {@inheritDoc}
     */
    public Class<?> getIdentifierClass(final Class<?> entityClass) {
        return getIdentifierMetaData(entityClass).getType();
    }

    /**
     * {@inheritDoc}
     */
    public String getIdentifierName(final Class<?> entityClass) {
        return DatanucleusUtil.getMetaData(entityManager, entityClass).getPrimaryKeyMemberNames()[0];
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public Object getIdentifierValue(final Object entity) {
        final AbstractMemberMetaData metaData = getIdentifierMetaData(entity.getClass());
        return valueGetters.get(metaData.getClass()).getValue(metaData, entity);
    }

    private AbstractMemberMetaData getIdentifierMetaData(final Class<?> entityClass) {
        return DatanucleusUtil.getMetaData(entityManager, entityClass).getMetaDataForMember(
                getIdentifierName(entityClass));
    }

    private interface ValueGetter<T extends AbstractMemberMetaData> {
        /**
         * Get value from specific object using member like {@link Field} or {@link Method}.
         */
        Object getValue(T metaData, Object target);
    }

    private static final class PropertyValueGetter implements ValueGetter<PropertyMetaData> {
        /**
         * {@inheritDoc}
         */
        public Object getValue(final PropertyMetaData metaData, final Object target) {
            final Method getter = ReflectionUtils.findMethod(target.getClass(), "get"
                    + StringUtils.capitalize(metaData.getName()));
            ReflectionUtils.makeAccessible(getter);
            return ReflectionUtils.invokeMethod(getter, target);
        }
    }

    private static final class FieldValueGetter implements ValueGetter<FieldMetaData> {
        /**
         * {@inheritDoc}
         */
        public Object getValue(final FieldMetaData metaData, final Object target) {
            final Field field = ReflectionUtils.findField(target.getClass(), metaData.getName());
            ReflectionUtils.makeAccessible(field);
            return ReflectionUtils.getField(field, target);
        }
    }
}
